Creating Custom Toolbox Images for Fedora Silverblue
Table of Contents
Toolbox is a fantastic way to make immutable desktop Linux distributions actually usable.
The standard Fedora Silverblue workflow involves running GUI applications using Flatpak, and creating mutable toolbox containers for day-to-day CLI tasks. While this is a fine approach, in my opinion, it doesn’t fully embrace the immutable paradigm shift.
When I first adopted silverblue, I followed the pattern of manually installing and configuring my personal toolbox container as a “pet”. This quickly becomes irritating in practice, especially when dealing with major version changes, and multiple toolbox containers. A better approach is to treat the environment itself like code.
Using a custom image with toolbox #
Toolbox supports creating a new container with a user supplied image, by passing
the --image
flag when creating a new container:
toolbox create --image <image-name>:<tag>
Since toolbox builds on Podman and other OCI technologies, you can build a toolbox compatible image from a standard Containerfile. This facilitates creating a custom environment without repetitive manual configuration.
Creating your own Containerfile #
Here is an example of a simple Containerfile that builds on the official toolbox image, installing some desired packages, and setting an environment variable:
Containerfile
FROM registry.fedoraproject.org/fedora-toolbox:38
ARG NAME=toolbox
LABEL name="$NAME" \
summary="Fedora toolbox container" \
maintainer="William Vandervalk"
ENV EDITOR=nvim
# Install packages
RUN dnf -y upgrade \
&& dnf -y install \
tmux \
nnn \
lsd \
tldr \
python3-pip \
nodejs \
gcc \
gcc-c++ \
ripgrep \
fd-find \
neovim \
&& dnf clean all
Building an image #
Local #
To build an image (and assign it the name toolbox
):
podman build -t toolbox -f /path/to/Containerfile
Now pass this image to toolbox and create a new container (also named
toolbox
):
toolbox create -i toolbox toolbox
Helper script #
To make things easier, a simple helper script placed alongside the Containerfile is useful for rebuilding locally:
build.sh
#!/bin/bash
# Set desired name via CLI argument, but default to "toolbox"
name="${1:-toolbox}"
echo "Cleaning existing image and container(s) if any exist"
toolbox rmi "$name" --force &> /dev/null
cd $(dirname "${BASH_SOURCE[0]}")
echo "Building image"
podman build -t "$name" -f Containerfile
echo "Creating toolbox"
toolbox create -i "$name" "$name"
To build a clean image and create a toolbox container (named toolbox
by
default) ensure your build script is executable, then run:
./build.sh
Or if you would like to specify a different name:
./build.sh toolbox-custom-name
Now, if you would like to modify your custom toolbox, rather than making ad-hoc changes, simply modify your Containerfile and rebuild.
This is a simple but powerful method for keeping your environment fully
reproducible. For example, when Fedora has a new release, simply increment the
version tag in the FROM
line of your Containerfile, rebuild, and you will have
an up to date toolbox including all of your customizations.
Building locally is simple and convenient, but an interesting alternative is to build and publish your image to a container registry using a CI/CD pipeline like Github Actions.
GitHub #
Create repository #
First create a new repository on GitHub, making sure to add your Containerfile.
Image signing #
To establish a trustworthy build chain, image signing via cosign is recommended.
Ensure that cosign is installed on your local workstation. Since it is written in go, downloading and running the single binary is easy.
Now create a key pair (password optional, but recommended):
cosign generate-key-pair
cosign.key
to your
public repository.
Once you have generated a key pair, using the GitHub web interface, add the key
as a secret to your repository, naming it COSIGN_PRIVATE_KEY
.
If you created your key with a password, add it as a separate secret, named
COSIGN_PASSWORD
.
Add workflow #
A custom workflow using GitHub Actions can be used to build, sign and publish container images to GitHub Packages whenever your Containerfile is modified via push or pull.
Make sure you have set up image signing, then add the
following build workflow to your repository, setting the desired image name
using the environment variable IMAGE_NAME
:
.github/workflows/build.yml
name: build
on:
workflow_dispatch:
push:
branches:
- main
paths:
- Containerfile
pull_request:
branches:
- main
paths:
- Containerfile
env:
IMAGE_NAME: toolbox
REGISTRY: ghcr.io
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Build image
id: build-image
uses: redhat-actions/buildah-build@v2
with:
image: ${{ env.IMAGE_NAME }}
tags: latest ${{ github.sha }}
containerfiles: |
./Containerfile
- name: Log in to the GitHub Container registry
uses: redhat-actions/podman-login@v1
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Push to GitHub Container Repository
id: push-to-ghcr
uses: redhat-actions/push-to-registry@v2
with:
image: ${{ steps.build-image.outputs.image }}
tags: ${{ steps.build-image.outputs.tags }}
registry: ${{ env.REGISTRY }}/${{ github.actor }}
- name: Install Cosign
uses: sigstore/[email protected]
- name: Sign container image
run: |
cosign sign -y --key env://COSIGN_PRIVATE_KEY ${{ env.REGISTRY }}/${{ github.actor }}/${{ env.IMAGE_NAME }}@${TAGS}
env:
TAGS: ${{ steps.push-to-ghcr.outputs.digest }}
COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
Adding custom binaries to image #
The example Containerfile demonstrates how
to add repository packages to your image, but in many cases software is not
packaged in the official repositories. Here is an example building on the
previous example, including cosign
from GitHub Releases:
FROM registry.fedoraproject.org/fedora-toolbox:38
ARG NAME=toolbox
ARG BIN_DIR=/usr/local/bin
ARG COMPLETIONS_DIR=/usr/local/share/bash-completion/completions
ARG COSIGN_VERSION=v2.0.2
LABEL name="$NAME" \
summary="Fedora toolbox container" \
maintainer="William Vandervalk"
ENV EDITOR=nvim
# Install packages
RUN dnf -y upgrade \
&& dnf -y install \
tmux \
nnn \
lsd \
tldr \
python3-pip \
nodejs \
gcc \
gcc-c++ \
ripgrep \
fd-find \
neovim \
&& dnf clean all
# Create bash-completion dir
RUN mkdir -p "${COMPLETIONS_DIR}"
# Install cosign
RUN curl -Lo "${BIN_DIR}/cosign" \
"https://github.com/sigstore/cosign/releases/download/${COSIGN_VERSION}/cosign-linux-amd64" \
&& chmod +x "${BIN_DIR}/cosign" \
&& cosign completion bash > "${COMPLETIONS_DIR}/cosign"
Note the added build arguments BIN_DIR
, COMPLETIONS_DIR
and
COSIGN_VERSION
.
Managing dependencies with Renovate #
Regardless of which build method you use (local or GitHub Actions
workflow), renovate is a fantastic way to keep your toolbox
Containerfile up to date. In the previous example, cosign
was included using a
binary downloaded from GitHub Releases. To configure renovate so that it will
monitor this, add the following renovate configuration to your repository:
.github/renovate.json
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:base"],
"regexManagers": [
{
"fileMatch": ["(^|/|\\.)Containerfile$", "(^|/)Containerfile[^/]*$"],
"matchStrings": [
"# renovate: datasource=(?<datasource>[a-z-]+?) depName=(?<depName>[^\\s]+?)(?: (lookupName|packageName)=(?<packageName>[^\\s]+?))?(?: versioning=(?<versioning>[^\\s]+?))?(?: registryUrl=(?<registryUrl>[^\\s]+?))?\\s(?:ENV|ARG) .+?_VERSION[ =]\"?(?<currentValue>.+?)\"?\\s"
]
}
]
}
Then add an instructive comment to your Containerfile, immediately preceding the version tag telling renovate where to look for new versions:
# renovate: datasource=github-releases depName=sigstore/cosign
ARG COSIGN_VERSION=v2.0.2
Now, whenever a new version of cosign
is published, renovate will create a
pull request automatically. If a build workflow is configured,
a new toolbox image should be built automatically after merging.
This pattern can be repeated for any number of dependencies relying on GitHub Releases.